﻿Imports System.Text.RegularExpressions
Imports System.Reflection
Imports System.Text

Namespace Util

    ''' <summary>
    ''' 文字列操作用ユーティリティクラス。
    ''' </summary>
    ''' <history>[sonodar] 2011/07/06 グラビアからコピーして新規作成</history>
    Public NotInheritable Class StringUtil

        '// パラメータマーカの正規表現パターン
        Private Shared ReadOnly ParamMakerPattern As Regex = New Regex("\{([0-9]+)\}")

        ''' <summary>
        ''' デフォルトコンストラクタを不可視とすることで、<br/>
        ''' インスタンスの生成を禁止する。
        ''' </summary>
        Private Sub New()
            '// 何もしない    
        End Sub

        ''' <summary>
        ''' objがNothingだった場合に、replaceの文字列を返す。<br/>
        ''' Nothingでなければobjの文字列を返す。
        ''' </summary>
        ''' <param name="obj">オブジェクト</param>
        ''' <param name="replace">補完するオブジェクト</param>
        ''' <returns>objの文字列。objがNothingだった場合はreplaceの文字列。</returns>
        <DebuggerStepThrough()> _
        Public Overloads Shared Function NullIf(ByVal obj As Object, ByVal replace As Object) _
            As String
            If (obj IsNot Nothing) Then
                Return obj.ToString
            End If
            If (replace Is Nothing) Then
                Throw New NullReferenceException("置き換える文字列がNULLを参照しています。")
            End If
            Return replace.ToString
        End Function

        ''' <summary>
        ''' オブジェクトの文字列表現を返す。<br/>
        ''' オブジェクトがNothingの場合は空文字を返す。
        ''' </summary>
        ''' <param name="obj">オブジェクト</param>
        ''' <returns>オブジェクトの文字列表現</returns>
        <DebuggerStepThrough()> _
        Public Shared Function NullToBlank(ByVal obj As Object) As String
            Return NullIf(obj, String.Empty)
        End Function

        ''' <summary>
        ''' 文字列を連結文字で連結して返す。
        ''' </summary>
        ''' <param name="delim">連結文字</param>
        ''' <param name="args">連結する文字列の可変長配列</param>
        ''' <returns>連結文字で連結された文字列</returns>
        <DebuggerStepThrough()> _
        Public Shared Function Join(ByVal delim As Object, ByVal ParamArray args() As Object) _
            As String

            If (args Is Nothing OrElse args.Length = 0) Then
                Return String.Empty
            End If

            'デリミタが指定されていなかったら空文字
            Dim _delim As String = NullToBlank(delim)

            Dim ReturnStr As StringBuilder = New StringBuilder()
            ReturnStr.Append(NullToBlank(args(0)))
            For i As Integer = 1 To args.Length - 1
                ReturnStr.Append(_delim)
                ReturnStr.Append(NullToBlank(args(i)))
            Next

            Return ReturnStr.ToString

        End Function

        ''' <summary>
        ''' 文字列を連結して返す。
        ''' </summary>
        ''' <param name="args">連結する文字列の可変長配列</param>
        ''' <returns>連結された文字列</returns>
        <DebuggerStepThrough()> _
        Public Shared Function Concat(ByVal ParamArray args() As Object) As String
            Return Join(String.Empty, args)
        End Function

        ''' <summary>
        ''' 文字列に含まれる{0}～{n}の置き換え文字に対してargsで指定された<br/>
        ''' パラメータをシーケンシャルにバインドする。
        ''' </summary>
        ''' <param name="str">置き換える文字列</param>
        ''' <param name="args">バインドするパラメータ</param>
        ''' <returns>パラメータで置き換えた文字列</returns>
        <DebuggerStepThrough()> _
        Public Shared Function Format(ByVal str As String, ByVal ParamArray args() As Object) _
            As String
            '// メッセージパラメータを検索
            Dim m As Match = ParamMakerPattern.Match(str)

            '// パラメータがなければそのまま返す
            If (Not m.Success) Then
                Return str
            End If

            '// 変換後の文字列
            Dim replace As StringBuilder = New StringBuilder()

            '// 前回検索文字列の最後尾位置
            Dim p As Integer = 0

            '// 一致があるだけ繰り返す
            Do While (m.Success)

                '// メッセージパラメータの添字を取得
                Dim i As Integer = Integer.Parse(m.Groups.Item(1).Value)

                '// 前回検索最後尾文字以降～今回検索文字先頭までの文字列を取得
                replace.Append(str.Substring(p, m.Index - p))

                If (args.Length > i) Then
                    replace.Append(NullToBlank(args(i)))
                Else
                    replace.Append(String.Empty)
                End If

                '// 前回検索位置を保持
                p = m.Index + m.Length

                '// 次の一致
                m = m.NextMatch
            Loop

            replace.Append(str.Substring(p))

            Return replace.ToString

        End Function

        ''' <summary>
        ''' 文字列が指定された範囲内かどうかをチェックする。
        ''' </summary>
        ''' <param name="str">範囲をチェックする文字列</param>
        ''' <param name="min">範囲の最小値(省略した場合は無制限)</param>
        ''' <param name="max">範囲の最大値(省略した場合は無制限)</param>
        ''' <param name="equalsCase">境界値をエラーとするかどうか</param>
        ''' <param name="ignoreCase">大文字・小文字を区別するかどうか</param>
        ''' <returns>範囲内ならTrue、範囲外ならFalse</returns>
        ''' <remarks>
        ''' min, maxにNothingが指定された場合は、それぞれ無制限となる。<br/>
        ''' min, maxともNothingだった場合、結果は常にTrueになる。
        ''' </remarks>
        Public Overloads Shared Function IsRange(ByVal str As String, _
                                                 Optional ByVal min As String = Nothing, _
                                                 Optional ByVal max As String = Nothing, _
                                                 Optional ByVal equalsCase As Boolean = False, _
                                                 Optional ByVal ignoreCase As Boolean = False) _
            As Boolean

            Dim compare As Integer
            '// 比較結果

            '// 最小値が指定されていた場合
            If (min IsNot Nothing) Then

                compare = String.Compare(str, min, ignoreCase)

                '// EqualsCaseがTrueの場合は同値をエラーとする
                If (equalsCase AndAlso compare = 0) Then
                    Return False
                End If

                '// 最小値より小さければNG
                If (compare < 0) Then
                    Return False
                End If

            End If

            '// 最大値が指定されていた場合
            If (max IsNot Nothing) Then

                compare = String.Compare(str, max, ignoreCase)

                '// EqualsCaseがTrueの場合は同値をエラーとする
                If (equalsCase AndAlso compare = 0) Then
                    Return False
                End If

                '// 最大値より大きければNG
                If (compare > 0) Then
                    Return False
                End If

            End If

            Return True

        End Function

        ''' <summary>
        ''' 文字列の右桁埋めを行う。
        ''' </summary>
        ''' <param name="obj">文字列に変換するオブジェクト</param>
        ''' <param name="length">文字列の桁数</param>
        ''' <param name="padChar">桁埋めする文字</param>
        ''' <returns>
        ''' 桁埋めされた文字列。<br/>
        ''' objの文字数がlengthより大きかった場合、length以降の文字列は切り捨てられる。
        ''' </returns>
        ''' <remarks></remarks>
        Public Shared Function RPad(ByVal obj As Object, ByVal length As Integer, _
                                    ByVal padChar As String) As String
            Dim str As String = NullToBlank(obj)

            If (str.Length > length) Then
                Return str.Substring(0, length)
            End If

            Dim len As Integer = length - str.Length
            Dim text As StringBuilder = New StringBuilder(str)

            For i As Integer = 0 To len - 1
                text.Append(padChar)
            Next

            Return text.ToString

        End Function

        ''' <summary>
        ''' 文字列の左桁埋めを行う。
        ''' </summary>
        ''' <param name="obj">文字列に変換するオブジェクト</param>
        ''' <param name="length">文字列の桁数</param>
        ''' <param name="padChar">桁埋めする文字</param>
        ''' <returns>
        ''' 桁埋めされた文字列。<br/>
        ''' objの文字数がlengthより大きかった場合、length以降の文字列は切り捨てられる。
        ''' </returns>
        ''' <remarks></remarks>
        Public Shared Function LPad(ByVal obj As Object, ByVal length As Integer, _
                                    ByVal padChar As String) As String
            Dim str As String = NullToBlank(obj)

            If (str.Length > length) Then
                Return str.Substring(0, length)
            End If

            Dim len As Integer = length - str.Length
            Dim text As StringBuilder = New StringBuilder(str)

            For i As Integer = 0 To len - 1
                text.Insert(0, padChar)
            Next

            Return text.ToString

        End Function

        ''' <summary>
        ''' 指定した文字数の文字列を左から取得する。
        ''' </summary>
        ''' <param name="obj">文字列を取得するオブジェクト</param>
        ''' <param name="length">文字数</param>
        ''' <returns>指定した文字数の文字列</returns>
        Public Shared Function Left(ByVal obj As Object, ByVal length As Integer) As String

            Dim str As String = obj.ToString

            If (str.Length > length) Then
                Return str.Substring(0, length)
            End If

            Return str
        End Function

        ''' <summary>
        ''' 指定した文字数の文字列を右から取得する。
        ''' </summary>
        ''' <param name="obj">文字列を取得するオブジェクト</param>
        ''' <param name="length">文字数</param>
        ''' <returns>指定した文字数の文字列</returns>
        Public Shared Function Right(ByVal obj As Object, ByVal length As Integer) As String

            Dim str As String = obj.ToString

            If (str.Length > length) Then
                Return str.Substring(str.Length - length)
            End If

            Return str
        End Function

        ''' <summary>
        ''' 指定した位置から指定した文字数の文字列を取得する。
        ''' </summary>
        ''' <param name="obj">文字列を取得するオブジェクト</param>
        ''' <param name="start">取得開始位置(1文字目を1とする)</param>
        ''' <param name="length">取得文字数(省略した場合は最後まで)</param>
        ''' <returns>指定した位置から取得した文字数分の文字列</returns>
        Public Shared Function Mid(ByVal obj As Object, _
                                   ByVal start As Integer, Optional ByVal length As Integer = -1) As String

            Dim str As String = obj.ToString

            '// 開始位置に満たない場合は空文字
            If (str.Length < start) Then
                Return String.Empty
            End If

            '// 長さ未指定、または文字数未満の場合は末尾まで
            If (length = -1) OrElse (str.Length - start < length - 1) Then
                Return str.Substring(start - 1)
            End If

            Return str.Substring(start - 1, length)

        End Function

        ''' <summary>
        ''' 文字列をseparatorで区切って返す。
        ''' </summary>
        ''' <param name="obj">文字列</param>
        ''' <param name="separator">区切り文字</param>
        ''' <param name="isCheckLength">文字数をチェックするかどうか</param>
        ''' <param name="lengths">
        ''' 分割文字数の可変長配列。説明を参照。<br/>
        ''' 1つの数値のみ渡された場合は、その文字数ごとに区切って返す。<br/>
        ''' 指定しなかった場合はstrがそのまま返される。
        ''' </param>
        ''' <returns>区切り文字で分割された文字列</returns>
        ''' <remarks>
        ''' strに分割したい文字列、separatorに区切り文字、lengths()には先頭から<br/>
        ''' 何文字ずつの区切りにするかを指定する。<br/>
        ''' 例えば、str:="1234567", separator:="-", length:=3,2,3 であれば、<br/>
        ''' "123-45-678"が返る。<br/>
        ''' isCheckLengthにTrueを指定した場合はlengthsの合計とstrの文字数をチェックし、<br/>
        ''' 差があった場合にNothingを返す。<br/>
        ''' lengthsの指定が1つの場合は、lengthsの倍数でなかった場合にNothingを返す。<br/>
        ''' Falseが指定された場合は先頭から強制的に分割していき、文字数が不足していた場合は<br/>
        ''' 途中で切れた文字列を、文字数が多かった場合は超えた分を切り捨てて返す。
        ''' </remarks>
        Public Shared Function Separate(ByVal obj As Object, ByVal separator As String, _
                                        ByVal isCheckLength As Boolean, _
                                        ByVal ParamArray lengths() As Integer) As String

            Dim str As String = obj.ToString()

            '// 文字数が未指定ならそのまま返す
            If (CollectionUtil.IsEmpty(lengths)) Then
                Return str
            End If

            '// 1. 複数の文字数が指定されていた場合の処理
            If (lengths.Length > 1) Then

                '// 文字列の文字数と、指定された文字数の合計が一致しなければNothing
                If (isCheckLength) Then
                    Dim sumlength As Integer = 0
                    For Each length As Integer In lengths
                        sumlength += length
                    Next
                    If (str.Length <> sumlength) Then
                        Return Nothing
                    End If
                End If

                '// 最初の分割文字数以下の場合はそのまま返す
                If (str.Length <= lengths(0)) Then
                    Return str
                End If

                Dim builder As StringBuilder = New StringBuilder()

                '// 先頭から最初の分割文字数までの文字列を追加
                builder.Append(str.Substring(0, lengths(0)))

                Dim currentIdx As Integer = lengths(0)  '// 現在の文字位置

                For i As Integer = 1 To lengths.Length - 1
                    builder.Append(separator)
                    If (str.Length <= (currentIdx + lengths(i))) Then
                        builder.Append(str.Substring(currentIdx))
                    Else
                        builder.Append(str.Substring(currentIdx, lengths(i)))
                    End If
                    currentIdx += lengths(i)
                Next

                Return builder.ToString

            End If

            '// 2. 指定された文字数が1つだけだった場合の処理

            Dim n As Integer = lengths(0)

            '// 分割文字数に満たない場合はそのまま返す
            If (str.Length <= n) Then
                Return str
            End If

            '// 指定された文字数での分割数を算出
            Dim count As Integer = CInt(Math.Ceiling(str.Length / n))

            '// 文字数の配列(すべて同値)を作成
            Dim arr() As Integer = CollectionUtil.FillArray(n, count)

            '// 自メソッド呼び出し
            Return Separate(str, separator, isCheckLength, arr)

        End Function

        ''' <summary>
        ''' 文字列を逆順にする。
        ''' </summary>
        ''' <param name="str">逆順にする文字列</param>
        ''' <returns>逆順になった文字列</returns>
        Public Shared Function Reverse(ByVal str As Object) As String
            Dim charArray() As Char = str.ToString().ToCharArray()
            Array.Reverse(charArray)
            Return New String(charArray)
        End Function

        ''' <summary>
        ''' 文字列を末尾からseparatorで区切って返す。
        ''' </summary>
        ''' <param name="str">文字列</param>
        ''' <param name="separator">区切り文字</param>
        ''' <param name="isCheckLength">文字数をチェックするかどうか</param>
        ''' <param name="lengths">
        ''' 分割文字数の可変長配列。説明を参照。<br/>
        ''' 1つの数値のみ渡された場合は、その文字数ごとに区切って返す。<br/>
        ''' 指定しなかった場合はstrがそのまま返される。
        ''' </param>
        ''' <returns>区切り文字で分割された文字列</returns>
        ''' <remarks>
        ''' strに分割したい文字列、separatorに区切り文字、lengths()には末尾から<br/>
        ''' 何文字ずつの区切りにするかを指定する。<br/>
        ''' 例えば、str:="1234567", separator:="-", length:=3,2,3 であれば、<br/>
        ''' "123-45-678"が返る。<br/>
        ''' isCheckLengthにTrueを指定した場合はlengthsの合計とstrの文字数をチェックし、<br/>
        ''' 差があった場合にNothingを返す。<br/>
        ''' lengthsの指定が1つの場合は、lengthsの倍数でなかった場合にNothingを返す。<br/>
        ''' Falseが指定された場合は末尾から強制的に分割していき、文字数が不足していた場合は<br/>
        ''' 途中からの文字列を、文字数が多かった場合は超えた分を先頭から切り捨てて返す。
        ''' </remarks>
        Public Shared Function SeparateReverse(ByVal str As Object, ByVal separator As String, _
                                        ByVal isCheckLength As Boolean, _
                                        ByVal ParamArray lengths() As Integer) As String
            Dim reverseStr As String = Reverse(str)
            reverseStr = Separate(reverseStr, separator, isCheckLength, lengths)
            If (reverseStr Is Nothing) Then
                Return Nothing
            End If
            Return Reverse(reverseStr)
        End Function

        ''' <summary>
        ''' 数値文字列をカンマなどで3桁に区切った通貨文字列に変換して返す。
        ''' </summary>
        ''' <param name="str">文字列</param>
        ''' <param name="separator">区切り文字。省略した場合はカンマ(,)。</param>
        ''' <param name="prefix">プレフィクス。省略した場合はなし。</param>
        ''' <param name="suffix">サフィクス。省略した場合はなし。</param>
        Public Shared Function ToCurrency(ByVal str As Object, _
                                        Optional ByVal separator As String = ",", _
                                        Optional ByVal prefix As String = Nothing, _
                                        Optional ByVal suffix As String = Nothing) As String
            Dim currency As String = SeparateReverse(str, separator, False, 3)

            Return Concat(prefix, currency, suffix)

        End Function

        ''' <summary>
        ''' 文字列から指定した文字を除外した文字列を返す。
        ''' </summary>
        ''' <param name="str">文字列</param>
        ''' <param name="chars">除外する文字</param>
        ''' <returns>除外文字が除外された文字列</returns>
        ''' <remarks>
        ''' str:="1,000,000", chars:={","}の場合、"1000000"を返す。<br/>
        ''' str:="2011/04/05 23:22:10", chars:={"/"c, " "c, ":"c} の場合、"20110405232210"を返す。<br/>
        ''' charsが指定されていなかった場合は文字列をそのまま返す。
        ''' </remarks>
        Public Shared Function RemoveChars(ByVal str As Object, ByVal ParamArray chars() As Char) As String
            '// 除外文字が未指定ならそのまま返す
            If (CollectionUtil.IsEmpty(chars)) Then
                Return str.ToString
            End If

            '// 除外文字をDictionaryに格納
            Dim charSet As IDictionary(Of Char, Char) = New Dictionary(Of Char, Char)
            Dim c As Char
            For Each c In chars
                charSet(c) = c
            Next

            Dim builder As StringBuilder = New StringBuilder()
            Dim e As CharEnumerator = str.ToString.GetEnumerator()

            '// 文字を1文字ずつ精査して、除外文字でなければ追加
            While (e.MoveNext)
                c = e.Current
                If (Not charSet.ContainsKey(c)) Then
                    builder.Append(c)
                End If
            End While

            Return builder.ToString()
        End Function

        ''' <summary>
        ''' 前後空白を削除した文字列を返す。<br/>
        ''' オブジェクトがNothingの場合でも空文字を返す。
        ''' </summary>
        ''' <param name="obj">前後空白を削除するオブジェクト</param>
        ''' <returns>前後空白が削除されたオブジェクト</returns>
        Public Shared Function Trim(ByVal obj As Object) As String
            If (obj Is Nothing) Then
                Return String.Empty
            End If
            Return obj.ToString.Trim()
        End Function

        ''' <summary>
        ''' バイナリデータを16進文字列に変換する。
        ''' </summary>
        ''' <param name="Binary">変換するバイナリデータ</param>
        ''' <param name="Delim">区切り文字</param>
        ''' <returns>16進数文字列</returns>
        Public Shared Function Dump(ByVal Binary() As Byte, Optional ByVal Delim As String = " ") As String

            '// NULLまたは空の場合は空文字を返す
            If (Binary Is Nothing OrElse Binary.Length = 0) Then Return String.Empty

            Const ZERO As String = "0"

            Dim str As New StringBuilder

            '// 先頭バイトを16進数として追加
            str.Append(LPad(Hex(Binary(0)), 2, ZERO))

            '// 2バイト目以降はデリミタと一緒に追加
            For i As Integer = 1 To Binary.Length - 1
                str.Append(Delim)
                str.Append(LPad(Hex(Binary(i)), 2, ZERO))
            Next

            Return str.ToString()

        End Function

        ''' <summary>
        ''' CSV出力用の行文字列を生成して返す。<br/>
        ''' 日付型はdateFormatにフォーマットされて生成される。
        ''' </summary>
        ''' <param name="obj">出力する値を保持しているDTOオブジェクト</param>
        ''' <param name="propertyNames">出力する値のプロパティ名配列</param>
        ''' <param name="dateFormat">日付書式(省略=yyyy/MM/dd HH:mm:ss)</param>
        ''' <param name="dateFormatMap">dateFormat以外を適用する日付プロパティとその書式</param>
        ''' <param name="delim">デリミタ。省略した場合はカンマ。</param>
        ''' <returns>CSV行文字列</returns>
        Public Shared Function ToCsvLine(ByVal obj As Object, _
                                      ByVal propertyNames() As String, _
                                      Optional ByVal dateFormat As String = "yyyy/MM/dd HH:mm:ss", _
                                      Optional ByVal dateFormatMap As IDictionary(Of String, String) = Nothing, _
                                      Optional ByVal delim As String = ",") As String

            Dim values(propertyNames.Length - 1) As String  '// 値格納配列
            Dim propName As String = Nothing                '// プロパティ名
            Dim value As Object = Nothing                   '// プロパティ値

            '// プロパティ名の数だけ繰り返す
            For i As Integer = 0 To propertyNames.Length - 1

                propName = propertyNames(i)                         '// プロパティ名を取得
                value = ReflectionUtil.GetProperty(obj, propName)   '// プロパティの値を取得

                If (value Is Nothing) Then

                    '// nullの場合は空文字を設定して次のプロパティへ
                    values(i) = String.Empty

                ElseIf (TypeOf value Is DateTime) Then

                    '// 日付型は日付書式でフォーマット
                    Dim format As String = dateFormat
                    If (dateFormatMap IsNot Nothing AndAlso dateFormatMap.ContainsKey(propName)) Then
                        format = dateFormatMap(propName)
                    End If
                    values(i) = DirectCast(value, DateTime).ToString(format)

                ElseIf (TypeOf value Is DateTime?) Then

                    '// Nullable日付型も日付書式でフォーマット
                    Dim format As String = dateFormat
                    If (dateFormatMap IsNot Nothing AndAlso dateFormatMap.ContainsKey(propName)) Then
                        format = dateFormatMap(propName)
                    End If
                    values(i) = DirectCast(value, DateTime?).Value.ToString(format)

                Else

                    '// 上記以外は文字列に変換して設定
                    values(i) = value.ToString()
                End If

            Next

            Return CollectionUtil.Join(Of String)(values, delim)

        End Function

    End Class

End Namespace